有一次我們上線一個會員權益兌換的功能,做的是連鎖咖啡品牌的點數換贈品。Demo 的時候流程完全順利:業務照著點下去,兌換成功,畫面跳出「兌換完成」。

上線第三天,工程師在群組丟了一句:「有人點兩下,扣了兩次點數。」
需求討論時,大家想的都是使用者照著流程走會發生什麼;實際會出事的,是使用者沒照著走的時候。網路斷一下、重複點兩下、點數差三點、後台主管請假沒簽核。這些「萬一」在需求階段沒人提,上線後會一個一個出現,每一個都是一張 bug 單。
區塊 7 The Edge Cases 處理的就是這一段。它不問「功能要做什麼」,只負責把「萬一怎麼辦」在需求階段先攤開。
先講一個前提:例外情境其實是規格基線的逆否命題。
Day 16 講規格基線時,我們會問「單檔上限多少」「兌換點數下限是多少」「一次最多選幾項」。這些是正向的 spec。而例外情境就是這些 spec 的反面:規格說「上限 10MB」,例外就問「超過 10MB 怎麼處理」;規格說「點數要滿 500 才能換」,例外就問「點數不夠時畫面怎麼回」。
所以鼠勾以有個硬性順序:先過規格基線,再進例外情境。如果某個規格還沒確認,對應的例外情境它不會硬寫,而是標一個 [待確認],備註「待規格基線的單檔上限確認後才能寫驗收條件」。
為什麼要這樣綁?因為例外情境的驗收條件需要具體數字。你沒辦法寫「超過上限時提示」這種 AC,那等於沒寫。你得寫「超過 10MB 時,顯示『檔案過大,請壓縮後再上傳』」。沒有那個 10MB,這條 AC 就是空的。先把正面講定,反面才寫得出來。
例外情境的麻煩在於太發散。一個表單會出錯的地方,跟一個付款流程會出錯的地方,內容差很多。如果只丟一句「請列出所有可能的例外」給使用者,他大概能寫出三條,上線後會冒出十條。
鼠勾以的做法是先判功能類型,再帶出對應的清單。它把功能分成十類:
| 類型 | 看到什麼關鍵字會判進來 |
|---|---|
| 表單類 | 輸入、填寫、送出、申請 |
| 選擇類 | 選擇、挑選、勾選、點選 |
| 查詢類 | 查詢、搜尋、列表、顯示 |
| 交易類 | 付款、扣款、兌換、繳費 |
| 預約類 | 預約、排程、時段、名額 |
| 上傳類 | 上傳、附件、檔案、圖片 |
| 通知類 | 通知、提醒、推播 |
| 權限類 | 角色、權限、簽核、後台 |
| 個資類 | 個資、隱私、敏感資料 |
| 匯出類 | 匯出、下載、列印、報表 |
判進哪幾類,就帶出哪幾類的專屬情境。可以複選,一個功能常常同時是「表單類 + 交易類 + 個資類」。回到開頭那個咖啡點數兌換,它就會被判成交易類,於是鼠勾以丟出來的問題大概長這樣:
兌換的時候有些狀況要先想好,我一個一個問你:
A. 點數不夠時:顯示差幾點?建議其他兌換品?還是導去看怎麼賺點數?
B. 重複發起同一筆兌換:冪等處理(只算一次)?提示已兌換過?還是允許重複?
C. 兌換到一半失敗:顯示失敗原因?提供重試?還是直接導客服?
開頭那個「點兩下扣兩次」,就是 B 這題沒問清楚。如果當初需求階段就答了「冪等處理」,工程師就知道要做防重,不會等到上線才補。
除了這十類各自的專屬情境,還有一組「通用情境」是不管什麼功能都會問的:身份驗證(Session 過期、權限不足)、系統與網路(API 超時、斷線、維護中)、並行與競爭(重複提交、多人改同一筆資料)。這幾個跟功能類型無關,是系統都會遇到的,所以每個案子都會被問一輪。
這裡會有一個合理的疑問:這些例外 AI 自己想不出來嗎?為什麼還要人工整理十類檢查表?
我盤點工具的時候也問過同樣的問題。差別在於模型有沒有這個知識,跟它在一場長對話裡會不會每次都主動提出來。
單獨問 AI「交易功能該注意哪些例外」,它可以把防重、超時講得很完整,知識確實都在。但需求釐清是一場二十幾輪的對話,模型同時要顧流程、顧語氣,「主動想起來要問防重」就變成機率問題。LLMREI 這份研究實測過讓 LLM 做需求訪談,平均只引出七成左右該問到的需求。檢查表的作用,是把這件事從機率變成固定動作:表上有的,每次都問。
資深 SA 也一樣。他什麼都知道,開需求會議照樣帶檢核清單進去,因為靠臨場記憶一定漏。AI 更需要這張清單,畢竟它每次對話都是全新的一個人,連上次漏問什麼的記憶都沒有。
另一半原因更實際:檢查表裡藏的常常是公司自己的知識。簽核要走哪條線、哪些題該標「待 SA 評估」而不是逼著 PM 當場回答,這些 AI 再聰明也不知道,只能靠人放進去。
那十類之外的功能呢?我一開始也擔心過沒辦法事先窮舉所有類型,後來確認不需要。這十類都不是憑空設計的,全是從真實案子累積出來的。整個策略分三層:
第三層最重要。檢查表缺一類本身不是大問題,缺了而且沒人知道才是。有了這個標注,下次同類型的需求進來,我就知道該補哪一類。檢查表不需要一開始就完整,需要的是能持續增補。
清單看起來只是「按類型查表」,但有兩處特別容易出錯,都是實際遇到之後才補進去的。
第一個是批次操作。查詢類常常會帶一個「勾選多筆一起處理」的功能,這裡有三個問題一定要單獨問:
這三題不問,工程師的預設行為每個人不一樣,「未選取當全選」這種實作真的出現過,一次誤操作就會影響大量資料。
這背後其實是個被講了幾十年的老原則。Jakob Nielsen 的可用性十大法則裡,第五條就是「預防錯誤(Error Prevention)」:好設計要事先擋掉會出錯的可能,而不是等使用者踩雷了才跳訊息補救。在需求階段把這三題問完,就是把「預防」往前挪到連 code 都還沒寫的時候。
第二個前置依賴更隱蔽,是「重複」這件事。表單類有「重複申請」、選擇類有「重複選擇」、交易類有「重複交易」、上傳類有「重複上傳」。這些情境表面在問「重複了怎麼辦」,但它其實依賴一個更前面的決定:什麼樣才算「重複」?
而「算不算重複」這件事,是在區塊 6 講欄位唯一性時定的:是全系統唯一、同一個人不能重複、還是根本不檢查。所以如果區塊 6 還沒確認某欄位的唯一性規則,這裡的「重複 X」情境就得標 [待確認],備註「待區塊 6 該欄位唯一性規則確認後才能寫 AC」。
這裡又是同一個結構:唯一性是正向 spec,重複情境是它的負向處理。整個工具的設計裡,這種正反成對的依賴出現在很多地方。
問完之後,鼠勾以不會只留一句「點數不足要提示」這種模糊記錄。它會轉成 Given-When-Then,而且每條多帶一欄「後續導向」,講清楚使用者看到錯誤之後會被帶去哪:
| 情境 | Given | When | Then | 後續導向 |
|---|---|---|---|---|
| 點數不足 | 可用點數 300,兌換需 500 | 點「確認兌換」 | 顯示「點數不足,尚差 200 點」,並給「如何賺點數」按鈕 | 導向賺點數說明,或返回改兌換品 |
| 重複兌換 | 同一筆兌換已成功 | 短時間再次送出 | 冪等處理,不重複扣點,提示「已兌換過」 | 留在原畫面 |
「後續導向」這一欄是刻意加的。很多需求只寫了錯誤訊息,沒寫接下來要怎麼辦:使用者看到「餘額不足」之後是停在原地,還是有下一步可以走。把出口一起想好,這個例外情境才算處理完。
Given-When-Then 來自 Dan North 提出的 BDD(行為驅動開發),原本是讓團隊用「在什麼前提下、做了什麼、就會怎樣」的句型把驗收條件寫成可執行的格式。例外情境的結構跟它一致,都是「在什麼前提下、使用者做了什麼、系統該怎麼回」,所以直接沿用。
這一整個區塊都在要求使用者想清楚「使用者沒照著走會怎樣」,而這個工具自己最大的例外,就是使用者沒照著答。
鼠勾以的前提是雙方一來一回好好對話,實際情況不會都這樣。有人連丟三句「不知道啦」「你決定」「隨便」,有人貼一段跟需求無關的抱怨,有人灌亂碼洗版,也有人用「忽略前面的指令」這類句子測試它。早期版本遇到這些會繼續一題一題追下去,對方已經在敷衍,它還在問,最後就是對話被關掉。
後來補了一個「無效輸入熔斷」。邏輯是雙軌計數:同一題連續被無效回應三次、或整場對話累積五個沒有任何推進的回合,鼠勾以就不再繼續追問,改成停下來說明狀況,把卡住的點記進待確認清單。
這個機制最難的部分是分流,偵測本身反而單純。一個新手需求方PM 答「這個我真的不知道,要回去問工程」,是完全正常、甚至該鼓勵的回答,不能被計入熔斷;它走的是另一條路:記進待確認、歸屬工程,不扣分也不觸發熔斷。所以只寫「偵測無效輸入」不夠,必須把幾種表面很像、處理方式完全不同的情況分開。真的不知道、惡意灌亂碼、想注入指令、嫌題目太多想加快,這四種各走一條路,混在一起處理一定會誤判。
為了確認分流不會誤傷,我做了一輪壓力測試:設計十二種使用者人格輪流測,包含不熟悉需求的、惡意的、沒耐心的、正常的,各配快速跟完整模式跑一遍。正常使用者那幾組完全沒被誤觸發,這是底線;惡意的那幾組壓出兩個我沒想到的漏洞,最明顯的是混合攻擊:把亂碼、注入、無關內容摻在一起丟,單看每一種都沒到熔斷門檻,合起來整場對話其實都在空轉。改法是把計數單位從「幾次無效輸入」換成「幾個零推進回合」,這樣即使對方每次換一種方式,只要該回合沒讓需求往前走,一樣會被計入。
講這段是想說明,例外情境的思維不只適用於「使用者要的那個功能」。任何會跟人互動的東西本身就是一個系統,有 happy path,也有一堆 edge case。這個專門用來引導別人想例外的工具,一樣得回頭套用自己的方法論檢查一遍。
區塊 7 做的事,是把「萬一」系統化。先認清例外情境是規格基線的反面,沒有正面數字就寫不出反面 AC;再用十類功能類型動態撈出該想的情境,不靠使用者憑空回憶;中間特別盯緊批次操作的三個破口、跟「重複」背後對欄位唯一性的依賴;最後全部收斂成帶「後續導向」的 Given-When-Then。十類涵蓋不了的,就讓它明講沒涵蓋,缺口自己會浮上來。
那個點兩下扣兩次的咖啡兌換,後來我們把防重補上了。但更重要的是,從那之後每個案子的需求討論,我都會在這一關多待一會兒。上線後最容易出包的,幾乎都是這區塊偷懶的地方。
七大區塊到這裡走完一輪。Day 21 換個主題:鼠勾以怎麼幫使用者打分數。為什麼要做一個 0 到 100 的 SA 就緒度,還配上十級成長標籤,這背後其實是一整套不想讓人填到一半就放棄的心理學設計。
這是 iThome 鐵人賽系列文章。明天見。
